package com.xuyanze.core;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.xuyanze.annotation.ApiDoc;
import com.xuyanze.annotation.FieldRemark;
import com.xuyanze.config.properties.ApiDocProperties;
import com.xuyanze.core.factory.ApiDocProcessor;
import com.xuyanze.core.manager.PathManager;
import com.xuyanze.domain.LoginContext;
import com.xuyanze.mock.MockManager;
import com.xuyanze.model.InterfaceDto;
import com.xuyanze.model.ParamDto;
import com.xuyanze.utils.HttpUtils;
import org.apache.http.message.BasicHeader;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.lang.reflect.*;
import java.util.*;

/**
 * @author ：lzy
 * @date ：2021-10-25
 * @description：api文档生成
 */
public class ApiDocGenerator {

    private ApiDocProperties apiDocProperties;

    private ApiDocProcessor apiDocProcessor;

    private LoginContext loginContext;

    public ApiDocGenerator(ApiDocProperties apiDocProperties) {
        this.apiDocProperties = apiDocProperties;
        loginContext = new LoginContext(this.apiDocProperties);
        apiDocProcessor = new DefaultApiDocProcessor();
    }

    private final static String PARAM_NAVIGATION= "->";
    private final static String API_DOC_PREFIX="API-DOC";
    private final static String API_DOC_KEY="%s-%s";
    private final static String API_DOC_DETAIL_URL="%s?projectId=%s&id=%s";

    public void postProcessAfterInitialization(Class<?> aClass) throws BeansException {
        Class<?> beanClass = aClass;
        if(beanClass.isAnnotationPresent(RestController.class) || aClass.isAnnotationPresent(RestController.class)){
            Method[] methods = beanClass.getDeclaredMethods();
            for (Method method : methods) {
                if (method.isAnnotationPresent(PostMapping.class) || method.isAnnotationPresent(GetMapping.class) || method.isAnnotationPresent(RequestMapping.class)) {
                    Class<?>[] parameterTypes = method.getParameterTypes();
                    if(parameterTypes.length == 0) continue;
                    // request
                    Class<?> parameterType = method.getParameterTypes()[0];
                    // response
                    Class<?> returnType = method.getReturnType();
                    Type genericReturnType = method.getGenericReturnType();

                    if(genericReturnType instanceof ParameterizedType){
                        Type actualTypeArgument = ((ParameterizedType) genericReturnType).getActualTypeArguments()[0];
                        char p = actualTypeArgument.getTypeName().charAt(0);
                        try {
                            if((p>= 'a' && p<='z') || (p>='A' && p<='Z')) returnType = (Class<?>) actualTypeArgument;
                        } catch (Exception e) {
                            ApiDoc apiDoc =parameterType.getAnnotation(ApiDoc.class);
                            if(apiDoc!=null){
                                System.out.println(String.format("response类型识别失败 %s-%s",aClass.getSimpleName(),actualTypeArgument));
                            }
                        }
                    }
                    generateApiDoc(beanClass,method,parameterType,returnType);
                }
            }
        }
    }


    private void generateApiDoc(Class<?> beanClass, Method method, Class<?> requestType, Class<?> responseType){
        ApiDoc apiDoc =requestType.getAnnotation(ApiDoc.class);
        if(apiDoc == null){
            return;
        }

        if(apiDocProperties.getGlobalPassSwitch() == 0){
            // 关闭总开关，用于指定ApiDoc 文档生成
            if(apiDocProperties.getGenSwitch() == 0 && apiDoc.forceSwitch() == -1){
                return;
            }

            if(apiDoc.forceSwitch() == 0){
                return;
            }

            if(!apiDoc.version().equals(apiDocProperties.getVersion())){
                return;
            }
        }


        RequestMapping requestMapping = beanClass.getAnnotation(RequestMapping.class);
        GetMapping getMapping = method.getAnnotation(GetMapping.class);
        PostMapping postMapping = method.getAnnotation(PostMapping.class);
        RequestMapping methodRequestMapping = method.getAnnotation(RequestMapping.class);
        String uri = "";
        if(requestMapping!=null) uri+=requestMapping.value()[0];
        if(getMapping!=null) uri+=getMapping.value()[0];
        if(postMapping!=null) uri+=postMapping.value()[0];
        if(methodRequestMapping!=null) uri+=methodRequestMapping.value()[0];

        // request 参数
        List<ParamDto> requestParams = requestParams(requestType);
        // response 参数
        List<ParamDto> responseParams = responseParams(responseType);

        // 正确请求实例
        String trueExam = buildTrueExam(responseType);
        // 错误请求实例
        String falseExam = buildFalseExam();


        String projectId = Optional.ofNullable(apiDoc.projectId().equals("")?null:apiDoc.projectId()).orElse(apiDocProperties.getProjectId());
        String moduleId = Optional.ofNullable(apiDoc.moduleId().equals("")?null:apiDoc.moduleId()).orElse(apiDocProperties.getModuleId());

        // 构建初始化接口对象
        InterfaceDto interfaceDto = InterfaceDto.build(apiDocProperties, uri, apiDoc.name(),apiDoc.wan());
        interfaceDto.setProjectId(projectId)
                .setModuleId(moduleId)
                .setParam(JSON.toJSONString(requestParams))
                .setResponseParam(JSON.toJSONString(responseParams))
                .setTrueExam(trueExam)
                .setFalseExam(falseExam)
                .setRemark(interfaceDto.getRemark() + "\n" + apiDoc.remark());

        BasicHeader[] headers = new BasicHeader[1];
        BasicHeader header= new BasicHeader("Cookie",loginContext.getCookie());
        headers[0] = header;

        // 请求参数
        Map<String, String> paramsMap = interfaceDto.toParamsMap();

        processBefore(paramsMap);
        String result = HttpUtils.postForm(apiDocProperties.getAddOrUpdateURL(), paramsMap, headers);
        if (!requestSuccess(result)) {
            //请求不成功，刷新cookie，重新请求
            loginContext.login();
            headers[0] = new BasicHeader("Cookie", loginContext.getCookie());
            result = HttpUtils.postForm(apiDocProperties.getAddOrUpdateURL(), paramsMap, headers);
        }

        processAfter(paramsMap,result);

    }

    /**
     * 是否请求成功
     * @param result
     * @return
     */
    private boolean requestSuccess(String result) {
        if(result != null) {
            JSONObject jsonObject = JSON.parseObject(result);
            int success = jsonObject.getIntValue("success");
            if(success == 1){
                return true;
            }
        }
        return false;
    }


    private void processBefore(Map<String, String> paramsMap) {
        String url = paramsMap.get("url");
        String pid = PathManager.getInstance().getPid(url);
        if(pid != null){
            paramsMap.put("id",pid);
        }

        apiDocProcessor.postProcessBefore(paramsMap);
    }

    private void processAfter(Map<String,String> paramMap, String result){
        if(result != null){
            JSONObject jsonObject = JSON.parseObject(result);
            int success = jsonObject.getIntValue("success");
            JSONObject data =null;
            if(success == 1){
                data = jsonObject.getJSONObject("data");
                String urlKey= data.getString("url");
                String idValue=data.getString("id");
                PathManager.getInstance().put(urlKey,idValue);
            }else{
                System.out.println(String.format("请求失败\n %s\n%s\n",JSON.toJSONString(paramMap),result));
            }

            boolean s = success == 1;
            String projectId = s ? data.getString("projectId") : null;
            String id = s ? data.getString("id") : null;
            String interfaceName = s ? data.getString("interfaceName") : null;
            String serverUrl =  s ? data.getString("url") : null;
            String apiDocUrl =  s ? String.format(API_DOC_DETAIL_URL,apiDocProperties.getApiHost(),projectId,id): null;
            apiDocProcessor.postProcessAfter(s,interfaceName,serverUrl,apiDocUrl);
        }
    }

    /**
     * 获取操作key
     * @param url
     * @return
     */
    private String getApiDocKey(String url){
        return String.format(API_DOC_KEY,API_DOC_PREFIX,url);
    }

    private String buildFalseExam() {
        return "{\n" +
                "  \"code\": \"201\",\n" +
                "  \"msg\": \"failed\"\n" +
                "}";
    }

    private String buildTrueExam(Class<?> responseType) {
        Map<String,Object> map = new HashMap<>();
        Object o =null;
        try {
            o = buildMockBean(responseType);

            map.put("code","200");
            map.put("msg","success");
            map.put("data",o);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return JSON.toJSONString(map,true);
    }

    public <T> T buildMockBean(Class<T> responseType) throws InstantiationException, IllegalAccessException {
        Object o;
        List<Field> fields = getFields(responseType);
        o = responseType.newInstance();
        for (Field field : fields){
            fillValue(o, field);
        }
        return (T)o;
    }

    private void setBean(Object o,Class<?> clazz){
        try {
            List<Field> fields = getFields(clazz);
            for (Field field : fields){
                fillValue(o, field);
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    private void fillValue(Object o, Field field) throws IllegalAccessException {
        FieldRemark annotation = field.getAnnotation(FieldRemark.class);
        if(annotation == null )
            return;

        if(Modifier.isFinal(field.getType().getModifiers())){
            reflectSet(o,field,annotation);
        }else{
            Class<?> type = field.getType();
            Object o2 = null;
            Object actual=null;
            try {
                if(field.getGenericType() instanceof ParameterizedType){
                    ParameterizedType pt = (ParameterizedType)field.getGenericType();
                    if(pt.getRawType().equals(List.class)){
                        List<Object> list = new ArrayList<>(1);
                        o2= list;
                        for (int i = 0; i < annotation.size(); i++) {
                            Class clazz = (Class)pt.getActualTypeArguments()[0];
                            Object e = newInstance(clazz);
                            list.add(e);
                            type = clazz;
                            setBean(e,type);
                        }
                        actual=list.get(0);
                    }
                }
                else{
                    o2 = newInstance(type);
                    setBean(o2,type);
                }


                field.setAccessible(true);
                field.set(o,o2);

                if(actual!=null){
                    o2 = actual;
                }
            } catch (InstantiationException e) {
                e.printStackTrace();
            }

            List<Field> childField = getFields(type);
            for (Field field1 : childField){
                try {
                    if(Modifier.isFinal(field1.getType().getModifiers())){
                        FieldRemark field1Annotation = field1.getAnnotation(FieldRemark.class);
                        if(field1Annotation == null ) return;
                        reflectSet(o2, field1, field1Annotation);
                    }else{
                        Object _obj = newInstance(type);
                        fillValue(_obj,field1);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private Object newInstance(Class clazz) throws InstantiationException, IllegalAccessException {
        if(clazz.getGenericSuperclass()!=null && clazz.getGenericSuperclass() == Number.class){
            try {
                return clazz.getConstructors()[0].newInstance(0);
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
        return clazz.newInstance();
    }

    private void reflectSet(Object o2, Field field1, FieldRemark annotation) throws IllegalAccessException {
        String tName = field1.getType().getSimpleName();
        field1.setAccessible(true);
        boolean empty = annotation.defaultValue().length() == 0;
        String defaultValue = annotation.defaultValue();

        if(tName.equals("int") || tName.equals("Integer")){
            field1.set(o2,new Integer(empty ? MockManager.mockNumber() : defaultValue));
        }else if(tName.equals("long") || tName.equals("Long")){
            field1.set(o2,new Long(empty ? MockManager.mockNumber() : defaultValue));
        }else if(tName.equals("boolean") || tName.equals("Boolean")){
            field1.set(o2,new Boolean(empty ? MockManager.mockBoolean() : defaultValue));
        }else if(tName.equals("double") || tName.equals("Double")){
            field1.set(o2,new Double(empty ? MockManager.mockNumber() : defaultValue));
        }else if(tName.equals("byte") || tName.equals("Byte")){
            field1.set(o2,new Byte(empty ? "0" : defaultValue));
        }else if(tName.equals("String")){
            field1.set(o2,empty ? MockManager.mockString(field1.getName()) : defaultValue);
        }
        else{
            field1.set(o2,annotation.defaultValue());
        }
    }

    private List<ParamDto> requestParams(Class<?> requestType) {
        List<ParamDto> requestParamList = new ArrayList<>();
        Class<?> superclass = requestType.getSuperclass();
        if(superclass!=null && superclass.getSimpleName().equals("OneLevelParamBean")){
            ParamDto oneLevelParam = new ParamDto("一级公参","true","string","http://","");
            requestParamList.add(oneLevelParam);
        }

        List<Field> fields = getFields(requestType);
        for (Field field : fields){
            List<ParamDto> navigation = getParamList(field,"");
            requestParamList.addAll(navigation);
        }
        return requestParamList;
    }

    private List<ParamDto> responseParams(Class<?> requestType) {
        List<ParamDto> requestParamList = new ArrayList<>();
        ParamDto codeParam = new ParamDto("code","true","string","返回码","");
        ParamDto msgParam = new ParamDto("msg","true","string","msg","");
        requestParamList.add(codeParam);
        requestParamList.add(msgParam);

        List<Field> fields = getFields(requestType);
        if(fields.size() > 0){
            ParamDto dataParam = new ParamDto("data","true","object","响应数据结构","");
            requestParamList.add(dataParam);
        }

        for (Field field : fields){
            List<ParamDto> navigation = getParamList(field,"data->");
            requestParamList.addAll(navigation);
        }
        return requestParamList;
    }


    public List<ParamDto> getParamList(Field field,String prefixStr){
        List<ParamDto> list = new ArrayList<>(1);
        StringBuilder prefix = new StringBuilder(prefixStr);
        getNavigationStr(list,field,prefix);
        return list;
    }

    public void getNavigationStr(List<ParamDto> list,Field field,StringBuilder prefix){
        FieldRemark annotation = field.getAnnotation(FieldRemark.class);
        if(annotation == null)
            return;

        String fieldType = getFieldType(field.getType());
        ParamDto param = new ParamDto();
        Class<?> type = field.getType();

        param.setName(prefix+field.getName());
        param.setType(fieldType);
        param.setNecessary(Optional.ofNullable(annotation).map(k->String.valueOf(k.necessary())).orElse("true"));
        param.setRemark(Optional.ofNullable(annotation).map(k->k.remark()).orElse(field.getName()));
        list.add(param);

        if(!Modifier.isFinal(type.getModifiers())){
            if(field.getGenericType() instanceof ParameterizedType){
                ParameterizedType pt = (ParameterizedType)field.getGenericType();
                if(pt.getRawType().equals(List.class)){
                    Class<?> clazz = (Class)pt.getActualTypeArguments()[0];
                    paramProcess(list, field, prefix, clazz);
                }
            }else{
                paramProcess(list, field, prefix, type);
            }
        }

    }

    private void paramProcess(List<ParamDto> list, Field field, StringBuilder prefix, Class<?> type) {
        List<Field> fields = getFields(type);
        StringBuilder copy = new StringBuilder(prefix);
        copy.append(field.getName() + PARAM_NAVIGATION);
        for (Field _field : fields) {
            getNavigationStr(list, _field, copy);
        }
    }

    private static List<Field> getFields(Class<?> parameterType) {
        List<Field> fields = new ArrayList<>();
        loopGet(fields,parameterType);
        return fields;
    }

    private static void loopGet(List<Field> fields, Class<?> parameterType) {
        if(parameterType==null)
            return;

        if(parameterType.getSimpleName().equals("OneLevelParamBean"))
            return;

        for(Field field : parameterType.getDeclaredFields()){
            fields.add(field);
        }
        loopGet(fields,parameterType.getSuperclass());
    }

    public static String getFieldType(Class<?> o) {
        if (!Modifier.isFinal(o.getModifiers())) {
            boolean isList = o.isAssignableFrom(List.class);
            boolean isArray = o.isArray();
            return (isList || isArray)?"array":"object";
        }

        return o.getSimpleName().toLowerCase();
    }

    public void handler(Set<Class<?>> typesAnnotatedWith) {
        for (Class<?> bean : typesAnnotatedWith) {
            postProcessAfterInitialization(bean);
        }
        PathManager.getInstance().flush();
    }
}
